package com.log4jviewer.logfile;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import com.log4jviewer.logfile.fields.AbstractField;
import com.log4jviewer.logfile.fields.DateField;
import com.log4jviewer.logfile.fields.LevelField;
import com.log4jviewer.logfile.fields.LogFieldName;
import com.log4jviewer.logfile.fields.NamedField;
import com.log4jviewer.logfile.fields.NumberedField;
import com.log4jviewer.logfile.fields.WildcardField;

/**
 * Class provides pattern parsing for using it to read text logs.
 * 
 * @author Apache
 */
public class PatternParser {

    private static final int LITERAL_STATE = 0;

    private static final int CONVERTER_STATE = 1;

    private static final int DOT_STATE = 3;

    private static final int MIN_STATE = 4;

    private static final int MAX_STATE = 5;

    private String pattern;

    private int state;

    private StringBuilder regexChars;

    private int patternCharIndex;

    private boolean leftAlign;

    private boolean rightAlign;

    private int minWidth = -1;

    private int maxWidth = -1;

    private StringBuilder recordRegex;

    private List<AbstractField> logFields;

    private Pattern recordPattern;

    public PatternParser(final String pattern) {
        this.pattern = preConvertPattern(pattern);
        state = LITERAL_STATE;
        recordRegex = new StringBuilder();
        logFields = new ArrayList<AbstractField>();
        parse();
    }

    public List<AbstractField> getLogFields() {
        return logFields;
    }

    public Pattern getRecordPattern() {
        if (recordPattern == null) {
            if (recordRegex.toString().endsWith("\\n")) {
                int end = recordRegex.toString().length();
                int start = end - 2;
                recordRegex.replace(start, end, "(?:\\n)");
            }
            recordPattern = Pattern.compile(recordRegex.toString(), Pattern.DOTALL);
        }
        return recordPattern;
    }

    private void parse() {
        final String lineSep = System.getProperty("line.separator");
        final char escapeChar = '%';

        regexChars = new StringBuilder();
        patternCharIndex = 0;

        while (patternCharIndex < pattern.length()) {
            char currentPatternChar = pattern.charAt(patternCharIndex++);

            switch (state) {
            case LITERAL_STATE:
                // In literal state, the last char is always a literal.
                if (patternCharIndex == pattern.length()) {
                    regexChars.append(currentPatternChar);
                    continue;
                }

                if (currentPatternChar == escapeChar) {
                    // peek at the next char.
                    switch (pattern.charAt(patternCharIndex)) {
                    case escapeChar:
                        regexChars.append(currentPatternChar);
                        patternCharIndex++; // move pointer
                        break;
                    case 'n':
                        regexChars.append(lineSep);
                        patternCharIndex++; // move pointer
                        break;
                    default:
                        if (regexChars.length() != 0) {
                            addCharsToRecordRegex(regexChars.toString());
                        }
                        regexChars.setLength(0);
                        regexChars.append(currentPatternChar); // append %
                        state = CONVERTER_STATE;
                        resetFormat();
                    }
                } else {
                    regexChars.append(currentPatternChar);
                }
                break;

            case CONVERTER_STATE:
                regexChars.append(currentPatternChar);

                switch (currentPatternChar) {
                case '-':
                    leftAlign = true;
                    break;
                case '.':
                    state = DOT_STATE;
                    break;
                default:
                    if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
                        minWidth = currentPatternChar - '0';
                        if (!leftAlign) {
                            rightAlign = true;
                        }
                        state = MIN_STATE;
                    } else {
                        finalizeDescriptor(currentPatternChar);
                    }
                } // switch
                break;

            case MIN_STATE:
                regexChars.append(currentPatternChar);

                if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
                    minWidth = (minWidth * 10) + (currentPatternChar - '0');
                } else if (currentPatternChar == '.') {
                    state = DOT_STATE;
                } else {
                    finalizeDescriptor(currentPatternChar);
                }
                break;

            case DOT_STATE:
                regexChars.append(currentPatternChar);

                if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
                    maxWidth = currentPatternChar - '0';
                    if (!leftAlign) {
                        rightAlign = true;
                    }
                    state = MAX_STATE;
                } else {
                    state = LITERAL_STATE;
                }
                break;

            case MAX_STATE:
                regexChars.append(currentPatternChar);
                if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
                    maxWidth = (maxWidth * 10) + (currentPatternChar - '0');
                } else {
                    finalizeDescriptor(currentPatternChar);
                    state = LITERAL_STATE;
                }
                break;
            default:
                // no code
            }
        }

        if (regexChars.length() != 0) {
            addCharsToRecordRegex(regexChars.toString());
        }
    }

    private void addCharsToRecordRegex(final String chars) {
        recordRegex.append(SpecialCharsConverter.convertSpecialChars(chars));
    }

    private void addFieldRegex(final AbstractField logField) {
        logFields.add(logField);
        recordRegex.append(logField.getRegex());
    }

    private void finalizeDescriptor(final char patternChar) {
        final String isoDateFormat = "ISO8601";
        final String absTimeDateFormat = "ABSOLUTE";
        final String dateAndTimeDateFormat = "DATE";
        final String relativeTimeDateFormat = "RELATIVE";
        AbstractField logField = null;

        switch (patternChar) {
        case 'c':
            logField = new NamedField(LogFieldName.CATEGORY, leftAlign, rightAlign, extractPrecisionOption());
            break;

        case 'C':
            logField = new NamedField(LogFieldName.CLASS, leftAlign, rightAlign, extractPrecisionOption());
            break;

        case 'd':
            String dateFormatString = "yyyy-MM-dd HH:mm:ss,SSS"; // 'ISO8601' date format
            String dOpt = extractOption();

            if ((dOpt != null) && !dOpt.equalsIgnoreCase(isoDateFormat)) {
                if (dOpt.equalsIgnoreCase(absTimeDateFormat)) {
                    dateFormatString = "HH:mm:ss,SSS";
                } else if (dOpt.equalsIgnoreCase(dateAndTimeDateFormat)) {
                    dateFormatString = "dd MMM yyyy HH:mm:ss,SSS";
                } else if (dOpt.equalsIgnoreCase(relativeTimeDateFormat)) {
                    dateFormatString = "SSSS";
                } else {
                    dateFormatString = dOpt;
                }
            }

            logField = new DateField(LogFieldName.DATE, leftAlign, rightAlign, dateFormatString);
            break;

        case 'F':
            logField = new NamedField(LogFieldName.FILE, leftAlign, rightAlign, 2);
            break;

        case 'L':
            logField = new NumberedField(LogFieldName.LINE, leftAlign, rightAlign);
            break;

        case 'm':
            logField = new WildcardField(LogFieldName.MESSAGE, leftAlign, rightAlign);
            break;

        case 'M':
            logField = new NamedField(LogFieldName.METHOD, leftAlign, rightAlign, 1);
            break;

        case 'p':
            logField = new LevelField(LogFieldName.LEVEL, leftAlign, rightAlign);
            break;
        case 'r':
            logField = new NumberedField(LogFieldName.MILLISECONDS, leftAlign, rightAlign);
            break;

        case 't':
            logField = new WildcardField(LogFieldName.THREAD, leftAlign, rightAlign);
            break;

        case 'x':
            logField = new WildcardField(LogFieldName.NDC, leftAlign, rightAlign);
            break;

        case 'X':
            logField = new WildcardField(LogFieldName.MDC, leftAlign, rightAlign);
            break;

        default:
            addCharsToRecordRegex(regexChars.toString());
        }

        regexChars.setLength(0);
        // Add the pattern converter to the list.
        addFieldRegex(logField);
        // Next pattern is assumed to be a literal.
        state = LITERAL_STATE;
        // Reset format
        resetFormat();
    }

    private String preConvertPattern(final String pattern) {
        return pattern.replaceAll("%l", "%C.%M(%F:%L)");
    }

    private void resetFormat() {
        leftAlign = false;
        rightAlign = false;
        minWidth = -1;
        maxWidth = -1;
    }

    // The option is expected to be in decimal and positive. In case of error, 0 is returned.
    private int extractPrecisionOption() throws NumberFormatException {
        String opt = extractOption();
        int extractedPrecisionOption = 0;

        if (opt != null) {
            extractedPrecisionOption = Integer.parseInt(opt);

            if (extractedPrecisionOption <= 0) {
                extractedPrecisionOption = 0;
            }
        }
        return extractedPrecisionOption;
    }

    private String extractOption() {
        if ((patternCharIndex < pattern.length()) && (pattern.charAt(patternCharIndex) == '{')) {
            int end = pattern.indexOf('}', patternCharIndex);

            if (end > patternCharIndex) {
                String extractedOption = pattern.substring(patternCharIndex + 1, end);
                patternCharIndex = end + 1;
                return extractedOption;
            }
        }
        return null;
    }
}
